﻿// MIT License
// 开源地址：https://gitee.com/co1024/AbcMvc
// Copyright (c) 2021-2022 1024
// Abc.Mvc=Furion+EF+SqlSugar+Pear layui admin.

namespace Abc.Base;

/// <summary>
/// MVC登陆扩展
/// </summary>
public static class MvcLoginExtensions
{
    /// <summary>
    /// 登陆跳转地址验证
    /// </summary>
    /// <param name="returnUrl"></param>
    /// <param name="defaultReturnUrl">如果验证失败，默认的跳转地址</param>
    /// <returns></returns>
    public static string LoginReturnUrlVer(this string returnUrl, string defaultReturnUrl)
    {
        var domain = App.HttpContext.GetDomain().TrimEnd("/").ToLower();
        //跳转地址为空
        if (string.IsNullOrWhiteSpace(returnUrl))
        {
            if (defaultReturnUrl.ToLower().StartsWith(domain))
            {
                return defaultReturnUrl;
            }
            else
            {
                return domain.ToLower() + "/" + defaultReturnUrl.TrimStart("/");
            }
        }
        else
        {
            //跳转地址不包含当前域名
            if (returnUrl.ToLower().StartsWith(domain))
            {
                return returnUrl;
            }
            else
            {
                //使用默认地址
                if (defaultReturnUrl.ToLower().StartsWith(domain))
                {
                    return defaultReturnUrl;
                }
                else
                {
                    return domain.ToLower() + "/" + defaultReturnUrl.TrimStart("/");
                }
            }
        }
    }

    /// <summary>
    /// 获取MVC登陆安全验证配置
    /// </summary>
    /// <param name="code"></param>
    /// <returns></returns>
    public static MvcLoginSafeVerItem GetMvcLoginSafeVerConfig(string code)
    {
        var mvcLoginSafeVerOptions = App.GetOptionsMonitor<MvcLoginSafeVerOptions>();
        var config = mvcLoginSafeVerOptions.configs.FirstOrDefault(o => o.Code.Trim().ToLower() == code.Trim().ToLower());
        if (config == null) config = new MvcLoginSafeVerItem();
        return config;
    }

    #region 登陆暴力破解

    /// <summary>
    /// 获取MVC登陆缓存Key
    /// </summary>
    /// <param name="account"></param>
    /// <param name="code"></param>
    /// <returns></returns>
    private static string GetMvcLoginAccountCacheKey(string account, string code) => Caches.GetCacheKey($"MVC_{code}_LoginFail_{account}");

    /// <summary>
    /// 获取MVC登陆缓存Key
    /// </summary>
    /// <param name="account"></param>
    /// <param name="code"></param>
    /// <returns></returns>
    private static string GetMvcLoginIPCacheKey(string code) => Caches.GetCacheKey($"MVC_{code}_LoginFail_IP");

    /// <summary>
    /// 是否允许登陆
    /// </summary>
    /// <param name="account"></param>
    /// <param name="code">缓存code</param>
    /// <param name="errorMsg"></param>
    /// <returns></returns>
    public static bool IsAllowLogin(this string account, string code, out string errorMsg)
    {
        var accountCacheKey = GetMvcLoginAccountCacheKey(account, code);
        var ipCacheKey = GetMvcLoginIPCacheKey(code); ;
        var logContent = $"登陆账号：{account} \r\n登陆信息：{App.HttpContext.GetWebVisitInfo()}";

        //var _logger = AppEx.GetLogger($"登陆安全判断_{code}");
        var _logger = AppEx.GetLogger();
        errorMsg = "";

        #region IP判断

        {
            var loginIPFailCaches = Caches.Get<ConcurrentDictionary<string, DateTimeOffset>>(ipCacheKey);
            if (loginIPFailCaches != null)
            {
                var ip = App.HttpContext.GetIpV6();
                if (string.IsNullOrWhiteSpace(ip)) ip = App.HttpContext.GetIpV4();
                if (loginIPFailCaches.ContainsKey(ip))
                {
                    //验证禁止时间是否过期
                    var ExpireTime = loginIPFailCaches[ip];
                    if (ExpireTime > DateTimeOffset.Now)
                    {
                        var ts = ExpireTime - DateTimeOffset.Now;
                        errorMsg = $"当前账号登陆失败次数过多，请 {(int)ts.TotalMinutes} 分钟后再试！";
                        logContent += errorMsg;
                        _logger.Debug($"\r\n==================\r\n{logContent}");
                        return false;
                    }
                }
            }
        }

        #endregion IP判断

        #region 验证账号是否被限制

        {
            var loginFailCaches = Caches.Get<ConcurrentDictionary<string, LoginFailDto>>(accountCacheKey);
            if (loginFailCaches != null && loginFailCaches.ContainsKey(account))
            {
                //验证禁止时间是否过期
                var loginfailinfo = loginFailCaches[account];

                //获取MVC登陆安全验证配置
                var config = GetMvcLoginSafeVerConfig(code);

                //验证登陆失败次数是否超过设定的次数
                if (loginfailinfo.FailCount >= config.MaxFailCount)
                {
                    //return false;//超过设定的最大次数，不允许登陆
                    //验证禁止登陆过期时间
                    if (loginfailinfo.ExpireTime != null && loginfailinfo.ExpireTime > DateTimeOffset.Now)
                    {
                        //禁止登陆时间未过期，不允许登陆

                        var ts = loginfailinfo.ExpireTime.Value - DateTimeOffset.Now;
                        errorMsg = $"登陆错误次数超过 {config.MaxFailCount} 次，请 {(int)ts.TotalMinutes} 分钟后再试！";
                        logContent += errorMsg;
                        _logger.Debug($"\r\n==================\r\n{logContent}");
                        return false;
                    }
                    else
                    {
                        //禁止登陆时间已到，移除
                        loginFailCaches.TryRemove(account, out _);
                        //更新缓存
                        Caches.Set(accountCacheKey, loginFailCaches, new Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions()
                        {
                            SlidingExpiration = new TimeSpan(0, 2, 0, 0)
                        });
                    }
                }
            }
        }

        #endregion 验证账号是否被限制

        _logger.Debug($"\r\n==================\r\n 允许登陆");

        return true;//允许登陆
    }

    /// <summary>
    /// 登陆失败
    /// </summary>
    /// <param name="account">账号</param>
    /// <param name="code">缓存code</param>
    /// <param name="surplusFailCount">剩余可失败次数</param>
    /// <param name="accountMaxFailCount">账号最大失败次数</param>
    /// <param name="accountLimitLoginMinutes">账号限制登陆分钟</param>
    /// <param name="ipMaxFailAccountLimitCount">IP最大登陆失败账号被限制登陆数(已经被限制登陆的账号数)</param>
    /// <param name="ipLimitLoginMinutes">IP限制登陆分钟</param>
    public static void LoginFail(this string account, string code, out int surplusFailCount, int accountMaxFailCount = 5, int accountLimitLoginMinutes = 30, int ipMaxFailAccountLimitCount = 5, int ipLimitLoginMinutes = 60)
    {
        surplusFailCount = 0;
        var accountCacheKey = GetMvcLoginAccountCacheKey(account, code);
        var ipCacheKey = GetMvcLoginIPCacheKey(code); ;

        var logContent = $"登陆账号：{account} \r\n登陆信息：{App.HttpContext.GetWebVisitInfo()}";

        //获取MVC登陆安全验证配置
        var config = GetMvcLoginSafeVerConfig(code);

        //取出账号缓存
        var loginCaches = Caches.Get<ConcurrentDictionary<string, LoginFailDto>>(accountCacheKey);
        if (loginCaches == null) loginCaches = new ConcurrentDictionary<string, LoginFailDto>();
        if (loginCaches == null || loginCaches.Count <= 0)
        {
            logContent += "\r\n=====================\r\n不存在，第一次新增";
            //不存在，第一次新增
            loginCaches.TryAdd(account, new LoginFailDto(account));
            surplusFailCount = accountMaxFailCount;//登陆失败次数
            //更新缓存
            Caches.Set(accountCacheKey, loginCaches, new Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions()
            {
                SlidingExpiration = new TimeSpan(0, 2, 0, 0)
            });
        }
        else
        {
            var ip = App.HttpContext.GetIpV6();
            if (string.IsNullOrWhiteSpace(ip)) ip = App.HttpContext.GetIpV4();
            //判断同一个IP登陆失败账号个数
            if (loginCaches.Values.Count(o => o.Ip == ip && (o.ExpireTime != null && o.ExpireTime > DateTimeOffset.Now)) >= ipMaxFailAccountLimitCount)
            {
                //同一个IP登陆失败限制账号超过设定的数
                //判断是否添加了IP限制
                var loginIPFailCaches = Caches.Get<ConcurrentDictionary<string, DateTimeOffset>>(ipCacheKey);
                if (loginIPFailCaches == null) loginIPFailCaches = new ConcurrentDictionary<string, DateTimeOffset>();
                if (!loginIPFailCaches.ContainsKey(ip))
                {
                    //添加IP登陆限制
                    loginIPFailCaches.TryAdd(ip, DateTimeOffset.Now.AddMinutes(ipLimitLoginMinutes));
                    //更新缓存
                    Caches.Set(ipCacheKey, loginIPFailCaches, new Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions()
                    {
                        SlidingExpiration = new TimeSpan(0, 2, 0, 0)
                    });

                    logContent += "\r\n=====================\r\n添加IP登陆限制";
                }
            }
            {
                #region 处理账号

                if (loginCaches.ContainsKey(account))
                {
                    //存在账号，验证
                    var loginFailInfo = loginCaches[account];
                    loginFailInfo.FailCount++;
                    surplusFailCount = config.MaxFailCount - loginFailInfo.FailCount + 1;//登陆失败次数

                    //验证登陆失败次数是否超过设定的次数
                    if (loginFailInfo.FailCount >= config.MaxFailCount)
                    {
                        loginFailInfo.ExpireTime = DateTimeOffset.Now.AddMinutes(accountLimitLoginMinutes);
                    }
                    //验证禁止登陆过期时间
                    else
                    {
                        if (loginFailInfo.ExpireTime == null || loginFailInfo.ExpireTime > DateTimeOffset.Now)
                        {
                            //禁止登陆时间未过期，不做处理

                            logContent += "\r\n=====================\r\n禁止登陆时间未过期，不做处理";
                        }
                        else
                        {
                            //禁止登陆时间已到，移除
                            loginCaches.TryRemove(account, out _);
                            logContent += "\r\n=====================\r\n禁止登陆时间已到，移除";
                        }
                    }
                    //更新缓存
                    Caches.Set(accountCacheKey, loginCaches, new Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions()
                    {
                        SlidingExpiration = new TimeSpan(0, 2, 0, 0)
                    });
                }
                else
                {
                    //不存在账号
                    loginCaches.TryAdd(account, new LoginFailDto(account));

                    logContent += "\r\n=====================\r\n不存在账号限制，新增";
                }

                #endregion 处理账号
            }
        }

        //var _logger = AppEx.GetLogger($"登陆失败日志_{code}");
        var _logger = AppEx.GetLogger($"登陆失败日志_{code}");
        _logger.Debug(logContent);
    }

    /// <summary>
    /// 登陆成功
    /// </summary>
    /// <param name="account">账号</param>
    /// <param name="code">缓存code</param>
    public static void LoginSuccess(this string account, string code)
    {
        var accountCacheKey = GetMvcLoginAccountCacheKey(account, code);
        //var ipCacheKey = GetMvcLoginIPCacheKey(code);
        var loginCaches = Caches.Get<ConcurrentDictionary<string, LoginFailDto>>(accountCacheKey);
        if (loginCaches == null || loginCaches.Count <= 0) return;
        loginCaches.TryRemove(account, out _);

        var _logger = AppEx.GetLogger($"登陆失败日志_{code}");
        _logger.Debug($"登陆成功，移除登陆失败限制缓存\r\n登陆信息：{App.HttpContext.GetWebVisitInfo()}");
    }

    #endregion 登陆暴力破解

    /// <summary>
    /// 获取登陆用户ID
    /// </summary>
    /// <param name="httpContext"></param>
    /// <returns></returns>
    public static int GetLoginUserId(this HttpContext httpContext)
    {
        return httpContext.GetLoginUserId<int>();
    }

    /// <summary>
    /// 获取用户 Id
    /// </summary>
    public static T GetLoginUserId<T>(this HttpContext httpContext)
    {
        if (httpContext == null || httpContext.User == null || httpContext.User.Identity.IsAuthenticated == false) return default;
        if (httpContext.User.Claims.Any(o => o.Type == ClaimConst.Claim_UserId))
        {
            var v = httpContext.User.FindFirst(ClaimConst.Claim_UserId).Value;
            return v.To<T>();
        }
        return default;
    }

    /// <summary>
    /// 获取用户 UserNick
    /// </summary>
    public static string GetLoginUserNick(this HttpContext httpContext)
    {
        if (httpContext == null || httpContext.User == null || httpContext.User.Identity.IsAuthenticated == false) return "";
        if (httpContext.User.Claims.Any(o => o.Type == ClaimConst.Claim_Name))
        {
            var v = httpContext.User.FindFirst(ClaimConst.Claim_Name).Value;
            return v.To<string>();
        }
        return httpContext.GetLoginUserAccount();
    }

    /// <summary>
    /// 获取用户 账户
    /// </summary>
    public static string GetLoginUserAccount(this HttpContext httpContext)
    {
        //return httpContext.User.Identity.Name;
        return httpContext.GetValue<string>(ClaimConst.Claim_Account);
    }

    /// <summary>
    /// 用获取值
    /// </summary>
    /// <param name="httpContext"></param>
    /// <param name="key"></param>
    /// <returns></returns>
    public static string GetValue(this HttpContext httpContext, string key)
    {
        return httpContext.GetValue<string>(key) ?? "";
    }

    /// <summary>
    ///  用获取值
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    /// <param name="httpContext"></param>
    /// <param name="key"></param>
    /// <returns>返回 TEntity</returns>
    public static TEntity? GetValue<TEntity>(this HttpContext httpContext, string key)
    {
        if (httpContext == null || httpContext.User == null) return default(TEntity);
        if (httpContext.User.FindFirst(key) == null) return default(TEntity);
        var claim = httpContext.User.FindFirst(key);
        if (claim == null || string.IsNullOrEmpty(claim.Value)) return default(TEntity);
        return claim.Value.To<TEntity>();
    }
}